"""
Copyright 2009 Texas A&M University
 
Licensed under the Apache License, Version 2.0 (the "License"); you may not use 
this file except in compliance with the License. You may obtain a copy of the 
License at

http://www.apache.org/licenses/LICENSE-2.0
  
Unless required by applicable law or agreed to in writing, software distributed 
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 
CONDITIONS OF ANY KIND, either express or implied. See the License for the 
specific language governing permissions and limitations under the License. 
"""

from collections import deque 
import datetime
import time
from logging import getLogger

import zope.interface

from twisted.internet import threads, reactor

from meercat.job.interfaces import IJob



log = getLogger('meercat.job.incremental')

class IncrementalJob(object):
    zope.interface.implements(IJob)
    
    _max_workers = None
    _filled = False
    _limit = lambda: 0
    _finished   = deque()
    _dispatched = {}
    _pending    = deque()
    
    running = False
    
    def __init__(self,f,limit,offset=0,step=1000,max_workers=None,
                 deferredFactory=None):
        """Constructor for an IncrementalJob
        
        Keyword arguments:
        
        limit -- The maximum item for the job or a callable that requires no 
        arguments and returns a positive integer
        offset -- The beginning/offset into the job, useful for restarting
        step -- The size of each working set
        max_workers -- Either a positive integer for the maximum number of 
        workers that should be employed for this job or None if there should
        be no limit 
        """
        
        if max_workers is not None:
            self._max_workers = int(max_workers)
        
        if not callable(f):
            raise ValueError('f must be a callable that takes two named '
                             'arguments: limit, offset')
        
        self._f = f
        
        if deferredFactory is None:
            self._factory = threads.deferToThread
        else:
            self._factory = deferredFactory
        
        self._offset = int(offset)
        self._step   = int(step)
        
        if callable(limit):
            self._limit = limit
        else:
            self._limit = lambda: int(limit)
        
        if self._step <= 0:
            raise ValueError('%s is an invalid step, it must be positive' % 
                             (self._step,))
        
        if self._offset < 0:
            raise ValueError('%s is an invalid starting point, it must be a '
                             'positive integer or 0' % (self._offset,))
        
        
    
    def __getstate__(self):
        d = self.__dict__.copy()
        if '_dispatched' in d:
            del d['_dispatched']
        if '_filled' in d:
            del d['_filled']
        
        if '_finished' in d:
            del d['_finished']
        
        if '_pending' in d:
            del d['_pending']
        
        if 'running' in d:
            del d['running']
        
        if '_factory' in d:
            del d['_factory']
        
        if '_limit' in d:
            d['_limit'] = d['_limit']()
        
        return d
    
    def __setstate__(self, state):
        for key, value in state.iteritems():
            if key == '_limit':
                value = lambda:value
            
            self.__dict__[key] = value
    
    def _callback(self,result,offset=None,limit=None):
        log.debug('%s finished on %s-%s' % (repr(self), str(offset), str(offset + limit),))
        self._finished.append(offset)
        del self._dispatched[offset]
        self._pop()
        
        return result
    
    def _errback(self,error,offset=None,limit=None):
        log.error('%s error on %s-%s' % (repr(self), str(offset), str(offset + limit),))
        log.error(error)
        del self._dispatched[offset]
        self._pop()
        
        return error
    
    def _generateDeferred(self, *args, **kwargs):
        return self._factory(self._f, *args, **kwargs)
    
    def _pop(self):
        try:
            next = self._pending.popleft()
        except IndexError:
            if len(self._dispatched) == 0:
                self.running = False
            return
        
        log.debug('%s starting %s-%s' % (repr(self), str(next), str(next + self._step),))
        
        d = self._generateDeferred(offset=next, limit=self._step)
        self._dispatched[next] = d
        d.addCallback(self._callback,offset=next, limit=self._step)
        d.addErrback(self._errback,offset=next, limit=self._step)
    
    def start(self):
        assert not self.running
        self.running = True
        
        log.debug('Starting %s' % (repr(self)))
        
        if not self._filled:
            self._pending.extend(range(self._offset,self._offset + self._limit(),self._step))
            self._filled = True
        
        for d in self._dispatched.itervalues():
            d.unpause()
        
        if self._max_workers is None:
            while len(self._pending) > 0:
                self._pop()
        else:
            while len(self._dispatched) < self._max_workers and \
                  len(self._pending) > 0:
                self._pop()
    
    def stop(self):
        self.running = False
        
        log.debug('Stopping %s' % (repr(self)))
        
        for d in self._dispatched.itervalues():
            d.pause()
    
    def isRunning(self):
        return bool(self.running)
    
    def getStatus(self):
        lengths = (len(self._finished),len(self._dispatched),
                   len(self._pending),)
        total = sum(lengths)
        
        if total > 0:
            percentages = tuple([100.0 * float(x)/float(total) for x in lengths])
        else:
            percentages = (0.00,0.00,0.00,)
        
        return u'%.2f%% finished, %.2f%% running, %.2f%% pending (%d, %d, %d)' % (
               percentages + lengths)

class IncrementalDailyDowntimeJob(IncrementalJob):
    _downtime_begin = None
    _downtime_duration = None
    _downtime_started = None
    _restart_scheduled = False
    
    def __init__(self,f,downtime_begin,downtime_duration,*args,**kwargs):
        if not isinstance(downtime_begin, datetime.time):
            raise ValueError('downtime_begin must be a datetime.time object')
        
        if not isinstance(downtime_duration, datetime.timedelta):
            raise ValueError('downtime_duration must be a datetime.timedelta object')
        
        assert downtime_duration < datetime.timedelta(days=1), u'Downtime duration must be less than a day'
        assert downtime_duration >= datetime.timedelta(microseconds=1), u'Downtime duration must be positive'
        
        super(IncrementalDailyDowntimeJob,self).__init__(f,*args,**kwargs)
        
        self._downtime_begin = downtime_begin
        self._downtime_duration = downtime_duration
    
    def _scheduleRestart(self):
        log.debug('Scheduling a restart of %s' % (repr(self),))
        if self._downtime_started is not None and not self._restart_scheduled:
            self._restart_scheduled = True
            diff = time.mktime((self._downtime_started + 
                                self._downtime_duration).timetuple()) - time.time()
            
            diff = max(diff,0.0)
            log.debug('Schedule end of downtime in %f seconds' % (diff))
            reactor.callLater(diff,self._endDowntime)
            
    
    def _startDowntime(self):
        log.debug('Starting downtime on %s' % (repr(self),))
        self._downtime_started = datetime.datetime.combine(datetime.date.today(),
                                                           self._downtime_begin)
        self.stop()
        self._scheduleRestart()
    
    def _endDowntime(self):
        log.debug('Ending downtime on %s' % (repr(self),))
        if not self._inDowntime():
            self._restart_scheduled = False
            self._downtime_start = False
            self.start()
        else:
            self._scheduleRestart()
    
    def _inDowntime(self):
        today_start = datetime.datetime.combine(datetime.date.today(),
                                                self._downtime_begin)
        today_end   = today_start + self._downtime_duration
        
        now = datetime.datetime.now()
        
        log.debug('_inDowntime: %s, %s, %s' % (str(today_start), str(today_end), str(now),))
        
        return now > today_start and now < today_end
    
    def _pop(self):
        if self._inDowntime():
            self._startDowntime()
        else:
            super(IncrementalDailyDowntimeJob,self)._pop()
    
    def start(self):
        if not self._inDowntime():
            super(IncrementalDailyDowntimeJob,self).start()    